package clang;

import view.InfoView;
import fixedpoint.DataflowException;
import fixedpoint.Drift;
import fixedpoint.ir.Node;
import floatingpoint.Buffer;
import floatingpoint.Filter;
import fixedpoint.ir.transformation.DataflowTransformer;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * TODO Create CodeGeneratorSettings frame with settings
 * TODO Investigate the possibility of having union variables (for less occupied space)
 */
public class CCodeGenerator extends Thread {

    private DataflowTransformer dataflowTransformer;
    private InfoView infoView;
    public static final String NEWLINE = System.getProperty("line.separator");
    public static final String CDATA_NEWLINE = "\n";
    public static final String OUTPUT_DIR = "output";

    public CCodeGenerator(DataflowTransformer dataflowTransformer) {
        this.dataflowTransformer = dataflowTransformer;
        this.infoView = dataflowTransformer.getInfoView();
    }

    /**
     * This function generates a XML file with:
     * <ul>
     * <li>filter information</li>
     * <li>fixed-point code (all available dataflows)</li>
     * </ul>
     * 
     * @throws ParserConfigurationException
     * @throws TransformerConfigurationException
     * @throws IOException
     * @throws TransformerException 
     */
    public void generateXML() throws ParserConfigurationException, TransformerConfigurationException, IOException, TransformerException {
        floatingpoint.Dataflow floatingpointDataflow = dataflowTransformer.getFloatingpointDataflow();

        PrintWriter outputStream = null;
        try {
            DocumentBuilderFactory factory =
                    DocumentBuilderFactory.newInstance();

            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document;
            document = builder.newDocument();

            // Set document content
            Element elementDataflow = document.createElement("dataflow");
            if (floatingpointDataflow instanceof Filter) {
                Filter filter = (Filter) floatingpointDataflow;
                final int order = filter.getInputBuffer().getLength() - 1;
                elementDataflow.setAttribute("order", Integer.toString(order));
                document.appendChild(elementDataflow);
                Element elementInputBuffer = document.createElement("inputBuffer");
                Buffer inputBuffer = filter.getInputBuffer();
                elementInputBuffer.setAttribute("low", Double.toString(inputBuffer.getStateRuntimeInterval().getLow()));
                elementInputBuffer.setAttribute("high", Double.toString(inputBuffer.getStateRuntimeInterval().getHigh()));
                elementDataflow.appendChild(elementInputBuffer);
                if (filter.isRecursive()) {
                    Element elementOutputBuffer = document.createElement("outputBuffer");
                    Buffer outputBuffer = filter.getOutputBuffer();
                    elementOutputBuffer.setAttribute("low", Double.toString(outputBuffer.getStateRuntimeInterval().getLow()));
                    elementOutputBuffer.setAttribute("high", Double.toString(outputBuffer.getStateRuntimeInterval().getHigh()));
                    elementDataflow.appendChild(elementOutputBuffer);
                }
            }

            // Write the floating-point expression
            Element elementFloatingPointExpression = document.createElement(
                    "floatingPointExpression");
            elementFloatingPointExpression.appendChild(document.createCDATASection(floatingpointDataflow.floatingPointExpression()));
            elementDataflow.appendChild(elementFloatingPointExpression);

            // Write the one-line C code
            for (clang.ir.Dataflow clangDataflow : dataflowTransformer.getCLangDataflowList()) {
                Element elementFixedPointExpression = document.createElement("fixedPointExpression");
                elementDataflow.appendChild(elementFixedPointExpression); // add element immediately, no matter if dataflow is in proper state
//                try {
//                    elementFixedPointExpression.setAttribute("complexity", Float.toString(dataflow.getComplexity()));
//                } catch (DataflowException ex) {
//                }

//                // Write the cost function of the current dataflow
//                Element elementCostFunction = document.createElement(
//                        "costFunctionApplied");
//                CostFunction costFunction = dataflow.getCostFunctionApplied();
//                elementCostFunction.setAttribute("driftCoefficient", Float.toString(
//                        costFunction.getDriftCoefficient()));
//                elementCostFunction.setAttribute("complexityCoefficient", Float.toString(
//                        costFunction.getComplexityCoefficient()));
//                elementFixedPointExpression.appendChild(elementCostFunction);

                elementFixedPointExpression.setAttribute("datatype",
                        clangDataflow.getRoot().getDatatype().toString());
                elementDataflow.appendChild(elementFixedPointExpression);

                // use \
//                    fixedPointExpression = split(fixedPointExpression, 96);
            }

            // Use a Transformer for output
            TransformerFactory tFactory =
                    TransformerFactory.newInstance();
            Transformer transformer = tFactory.newTransformer();

            DOMSource source = new DOMSource(document);

            final String filename = OUTPUT_DIR + "\\" + "filter" + ".xml";
            outputStream = new PrintWriter(new FileWriter(filename));

            StreamResult result = new StreamResult(outputStream);
            transformer.transform(source, result);
        } finally {
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }

    /**
     * Generates fixed-point C functions (.h and .c file pair)
     */
    public void generateCLangModule() throws IOException, DataflowException {
        floatingpoint.Dataflow floatingpointDataflow = dataflowTransformer.getFloatingpointDataflow();

        PrintWriter osDecl = null;
        PrintWriter osDef = null;
        try {
            final String t = "fixed-point"; // common part in file names
            final String filenameDecl = OUTPUT_DIR + "/" + t + ".h";
            osDecl =
                    new PrintWriter(new FileWriter(filenameDecl));

            final String filenameDef = OUTPUT_DIR + "/" + t + ".c";
            osDef =
                    new PrintWriter(new FileWriter(filenameDef));

            osDecl.println("/*" + NEWLINE
                    + "Digital Filter Optimization Project - see http://code.google.com/p/c-lang-digital-filter-optimization/" + NEWLINE + NEWLINE
                    + "This file contains declarations of fixed-point functions that are generated automatically." + NEWLINE
                    + "To use the fixed-point functions, copy or include this file and file "
                    + t + ".c" /* the extension must be the same as in filenameDef */ + " in your C/C++ project." + NEWLINE
                    + "*/");
            String conditionalCompilationSymbol = "__FIXED_POINT__";
            osDecl.println("#ifndef " + conditionalCompilationSymbol + NEWLINE
                    + "#define " + conditionalCompilationSymbol + NEWLINE);

            // If this declaration is removed,
            //  then the (Microsoft Visual Studio 2010) linker can display
            //  "error LNK2001: unresolved external symbol ..."
            // The reason is probably the filename extension (.c)
            osDecl.println("#ifdef __cplusplus" + NEWLINE + "extern \"C\" {" + NEWLINE + "#endif");

            //osDef.println("#include <stdlib.h>");
            osDef.println("#include \"" + t + ".h" + "\"" + NEWLINE + NEWLINE);

            osDecl.println(floatingpointDataflow.CLangPreprocessorInfo());

            String floatingPointParam = floatingpointDataflow.CLangFuncParam(true);
            String fixedPointParam = floatingpointDataflow.CLangFuncParam(false);

            // floating-point code declaration
            osDecl.println(
                    "extern float floating_point(" + floatingPointParam + ");" + NEWLINE);

            // floating-point code definition
            osDef.println(
                    "float floating_point(" + floatingPointParam + ") { return " + floatingpointDataflow.floatingPointExpression() + "; }" + NEWLINE);

            osDecl.println("#define FIXED_POINT_FUNCTION_ARRAY_LENGTH  " + dataflowTransformer.getDataflowList().size());

            // Write the fixed-point functions
            StringBuilder functionNameList = new StringBuilder();
            StringBuilder complexityArray = new StringBuilder();
            StringBuilder driftArray = new StringBuilder();
            int i = 0;
            for (clang.ir.Dataflow clangDataflow : dataflowTransformer.getCLangDataflowList()) {
                try {
                    String fname = "fixed_point__" + i;
                    i++;
                    functionNameList.append("    ").append(fname).append(", ").append(NEWLINE);
                    fixedpoint.ir.Dataflow fixedpointDataflow = clangDataflow.getFixedpointDataflow();
                    complexityArray.append(fixedpointDataflow.getComplexity()).append(", ");
                    Drift drift = fixedpointDataflow.getDrift();
                    driftArray.append("{").append(drift.getLow()).append("f, ").append(drift.getHigh()).append("f},").append(NEWLINE);

                    osDef.println(clangDataflow.getCLangInfo());
                    osDef.print(clangDataflow.getRoot().getDatatype().toString());
                    osDef.print(" ");
                    osDef.print(fname);
                    osDef.print('(');
                    osDef.print(fixedPointParam);
                    osDef.println(')');
                    osDef.println("{");
                    osDef.println(
                            clangDataflow.buildCLangCode());
//                    osDef.print(indentation(1));
//                    osDef.print("return ");
//                    osDef.print(clangDataflow.CLangExpression__undeclared());
//                    osDef.println(';');
                    osDef.println("}");
                    osDef.println();

                } catch (DataflowException ex) {
                    osDef.println(
                            "/* Internal error: " + ex.getMessage() + " */" + NEWLINE);
                }
            }

            // Write the array of function pointers
            ArrayList<fixedpoint.ir.Dataflow> fixedointDataflowList = dataflowTransformer.getDataflowList();
            if (!fixedointDataflowList.isEmpty()) {
                fixedpoint.ir.Dataflow fixedpointDataflow = fixedointDataflowList.get(0); // sample dataflow
                String returnType = null;
                if (fixedpointDataflow != null) {
                    final Node root = fixedpointDataflow.getRoot();
                    if (root != null) {
                        Datatype datatype = root.getRuntimeInterval().datatype();
                        if (datatype != null) {
                            returnType = datatype.toString();
                        }
                    }
                }
                if (returnType != null) {
                    osDecl.println("#define FILTER_OUTPUT_DATATYPE " + returnType);
                    osDecl.println(
                            "extern " + returnType + " (*fixed_point[])(" + fixedPointParam + ");" + NEWLINE);
                    osDef.println(
                            returnType + " (*fixed_point[])(" + fixedPointParam + ") = {" + NEWLINE);
                    osDef.println(functionNameList);
                    osDef.println("}; " + NEWLINE);
                } else {
                    osDef.println("/* Internal error: null return type */" + NEWLINE);
                }
            } else {
                osDecl.println("/* Empty filter dataflow list */");
            }

            // Write array of complexity values
            osDecl.println(
                    "extern float complexity[];" + NEWLINE);
            osDef.println(
                    "float complexity[] = { " + complexityArray + "};");

            // Write array of drift low/high values
            osDecl.println(
                    "extern float drift[][2]; /* For testing in C++ the error interval computed in Java */" + NEWLINE);
            osDef.println(
                    "float drift[][2] = { " + driftArray + "};");

            osDecl.println("#ifdef __cplusplus" + NEWLINE + "}" + NEWLINE + "#endif" + NEWLINE); // commented already

            osDecl.println("#endif" + NEWLINE);
        } finally {
            if (osDef != null) {
                osDef.close();
            }
            if (osDecl != null) {
                osDecl.close();
            }
        }
    }

    /**
     * 
     * @param dim max line dimension
     * @return 
     */
    private String split(String s, int dim) {
        int i = 0;
        final String delimiter = ")";
        while (i < s.length()) {
            i += dim;
            int j = s.indexOf(delimiter, i);
            if (j > 0) {
                j += delimiter.length(); // pass over delimiter
                s = s.substring(0, j) + "  \\" + CDATA_NEWLINE + s.substring(j);
                i = j;
            } else {
                break;
            }
        }
        return s;
    }

    /**
     */
    public void generate(String filename, String content) {
        PrintWriter os = null;
        try {
            os = new PrintWriter(new FileWriter(OUTPUT_DIR + "/" + filename));
            os.print(content);
        } catch (IOException ex) {
            infoView.showExceptionDialog(ex);
        } finally {
            if (os != null) {
                os.close();
            }
        }
    }

    @Override
    public void run() {
        try {
//            generateXML();
            generateCLangModule();
        } catch (Exception ex) {
            infoView.showExceptionDialog(ex);
            ex.printStackTrace();
        }

        infoView.showMessage("C code generation is complete");
    }

    public static String indentation(int level) {
        StringBuilder strBuilder = new StringBuilder();
        for (int i = 0; i < level; i++) {
            strBuilder.append("    "); // 4 spaces
        }
        return strBuilder.toString();
    }
}
